import 'dart:typed_data';

import 'dart:math' show min;
import 'package:archive/archive.dart';
import 'package:dbio_utils/generic_filehandle/generic_file_handle.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';

import '../generic_filehandle/local_file.dart';
import 'gzi_index.dart';

class BgzFileHandle {
  GenericFileHandle fileHandle;
  GziIndex gzi;
  BgzFileHandle({
    @required this.fileHandle,
    @required GenericFileHandle gziFileHandle,
  }) {
    gzi = GziIndex(fileHandle: gziFileHandle);
  }

  Future<Map> stat() async {
    var compressedStat = await this.fileHandle.stat();
    return {
      ...compressedStat,
      'size': await this.getUncompressedFileSize(),
      'blocks': null,
      'blksize': null,
    };
  }

  getUncompressedFileSize() async {
    // read the last block's ISIZE (see gzip RFC),
    // and add it to its uncompressedPosition
    List lastBlock = await this.gzi.getLastBlock();
    var uncompressedPosition = lastBlock[1];

    var size = this.fileHandle.size;

    // note: there should be a 28-byte EOF marker (an empty block) at
    // the end of the file, so we skip backward past that
    var buf = await this.fileHandle.read(0, 4, size - 28 - 4);

    if (buf.lengthInBytes != 4) throw new Exception('read error');
    var lastBlockUncompressedSize = ByteData.view(buf.buffer).getUint32(0, Endian.little);
    return uncompressedPosition + lastBlockUncompressedSize;
  }

  _readAndUncompressBlock(blockBuffer, [compressedPosition, nextCompressedPosition]) async {
    var next = nextCompressedPosition;
    if (!next) {
      next = this.fileHandle.size;
    }

    // read the compressed data into the block buffer
    var blockCompressedLength = next - compressedPosition;

    Uint8List buffer = await this.fileHandle.read(
          blockBuffer,
          0,
          blockCompressedLength,
          compressedPosition,
        );

    // uncompress it
    var unzippedBuffer = GZipDecoder().decodeBytes(
      buffer.sublist(0, blockCompressedLength),
    );

    return unzippedBuffer;
  }

  read(Uint8List buf, int offset, int length, int position) async {
    // get the block positions for this read
    var blockPositions = await this.gzi.getRelevantBlocksForRead(length, position);
    var blockBuffer = Uint8List(32768 * 2);
    // uncompress the blocks and read from them one at a time to keep memory usage down
    var destinationOffset = offset;
    var bytesRead = 0;
    for (var blockNum = 0; blockNum < blockPositions.length - 1; blockNum += 1) {
      // eslint-disable-next-line no-await-in-loop
      Uint8List uncompressedBuffer = await this._readAndUncompressBlock(
        blockBuffer,
        blockPositions[blockNum],
        blockPositions[blockNum + 1],
      );
      int uncompressedPosition = blockPositions[blockNum][1];
      var sourceOffset = uncompressedPosition >= position ? 0 : position - uncompressedPosition;
      int sourceEnd = min(position + length, uncompressedPosition + uncompressedBuffer.length) - uncompressedPosition;
      if (sourceOffset >= 0 && sourceOffset < uncompressedBuffer.length) {
        buf.replaceRange(destinationOffset, destinationOffset + sourceEnd - sourceOffset, uncompressedBuffer.sublist(sourceOffset, sourceEnd));
        //uncompressedBuffer.copy(buf, destinationOffset, sourceOffset, sourceEnd);
        destinationOffset += sourceEnd - sourceOffset;
        bytesRead += sourceEnd - sourceOffset;
      }
    }

    return {'bytesRead': bytesRead, 'buffer': buf};
  }
}
